package com.metaverse.backend.lock;

import com.google.common.base.Joiner;
import com.metaverse.backend.config.RedisLockKey;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.annotation.Order;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

/**
 * @author mashaohua
 */
@Aspect
@Component
@Slf4j
@Order(1)
public class RedisLockInterceptor{

    private static final LocalVariableTableParameterNameDiscoverer DISCOVERER = new LocalVariableTableParameterNameDiscoverer();

    private static final ExpressionParser PARSER = new SpelExpressionParser();

    @Autowired
    private RedissonClient redissonClient;

    @Pointcut("@annotation(com.metaverse.backend.lock.RedisLockable)")
    public void pointcut() {
    }

    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint point) throws Throwable {

        MethodSignature methodSignature = (MethodSignature) point.getSignature();
        Method targetMethod = AopUtils.getMostSpecificMethod(methodSignature.getMethod(), point.getTarget().getClass());
        String targetName = point.getTarget().getClass().getName();
        String methodName = point.getSignature().getName();
        Object[] arguments = point.getArgs();

        RedisLockable redisLock = targetMethod.getAnnotation(RedisLockable.class);
        String redisKey = getLockKey(redisLock, targetMethod, arguments);
        log.debug(">>>>尝试锁定执行方法：{}, 锁定：{}" ,methodName, redisKey);
        RLock rlock = redissonClient.getLock(redisKey);

        if (rlock.tryLock(redisLock.tryTime(), TimeUnit.MILLISECONDS)) {
            try {
                // manipulate protected state
                return point.proceed();

            } finally {
                rlock.unlock();
                log.debug(">>>>释放锁定执行方法：{}, 锁定key：{}, 锁定value：{},结果：{}" ,methodName, redisKey);
            }
        } else {
            // perform alternative actions
            log.info("锁定方法失败, methodName:{} , key：{}" , methodName, redisKey);
            if(!redisLock.ignoreFail()) {
                throw new RedisLockException(redisKey);
            }
        }

        return null;

    }

    private String getLockKey(RedisLockable redisLock, Method targetMethod, Object[] arguments) {
        String[] keys = redisLock.key();
        RedisLockKey prefix = redisLock.prefix();
        StringBuilder sb = new StringBuilder("lock.");

        sb.append(prefix.name() + ":");

        if (keys != null && keys.length > 0) {
            String keyStr = Joiner.on("+ '.' +").skipNulls().join(keys);
            if(StringUtils.isNotBlank(keyStr)){
                EvaluationContext context = new StandardEvaluationContext(targetMethod);
                String[] parameterNames = DISCOVERER.getParameterNames(targetMethod);
                for (int i = 0; i < parameterNames.length; i++) {
                    context.setVariable(parameterNames[i], arguments[i]);
                }
                Object key = PARSER.parseExpression(keyStr).getValue(context);
                sb.append("#").append(key);
            }
        }else{
            String signature = targetMethod.getDeclaringClass().getName() + "." + targetMethod.getName();
            sb.append(signature);
        }
        return sb.toString();
    }


}